namespace ScrumSprintMonitor.Implementation.AvailableTimeCalculator
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.Diagnostics;
	using System.Linq;

	using Infrastructure;
	using Infrastructure.Configuration;

	using Isolation.Outlook;

	using Properties;

	#endregion

	public class TeamMemberOutlookFreeBusyInfo : ITeamMemberFreeBusyInfo,
	                                             IDisposable
	{
		private static readonly TimeSpan SlotSize = TimeSpan.FromMinutes(15);
		private readonly IFreeBusyInfoConfiguration freeBusyInfoConfiguration;
		private readonly ILogger<TeamMemberOutlookFreeBusyInfo> logger;
		private readonly Dictionary<string, IRecipient> recipientDictionary = new Dictionary<string, IRecipient>();

		private readonly Dictionary<string, Dictionary<DateTime, RecipientFreeBusyCacheInfo>> recipientFreeBusyDictionary =
			new Dictionary<string, Dictionary<DateTime, RecipientFreeBusyCacheInfo>>();

		private IOutlookSession outlookSession;

		public TeamMemberOutlookFreeBusyInfo(IFreeBusyInfoConfiguration freeBusyInfoConfiguration, ILogger<TeamMemberOutlookFreeBusyInfo> logger)
		{
			if (freeBusyInfoConfiguration == null)
			{
				throw new ArgumentNullException("freeBusyInfoConfiguration");
			}
			if (logger == null)
			{
				throw new ArgumentNullException("logger");
			}

			this.freeBusyInfoConfiguration = freeBusyInfoConfiguration;
			this.logger = logger;
		}

		#region IDisposable Members

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		#endregion

		~TeamMemberOutlookFreeBusyInfo()
		{
			Dispose(false);
		}

		private void Dispose(bool disposing)
		{
			if (disposing)
			{
				if (this.outlookSession != null)
				{
					this.outlookSession.Dispose();
				}
			}
		}

		#region Implementation of ITeamMemberFreeBusyInfo

		public bool IsBusy(ITeamMemberInformation teamMemberInfo, DateTime dateTime)
		{
			if (teamMemberInfo.IsBusy(dateTime, SlotSize))
			{
				return true; // Lunch hour
			}

			var recipient = GetRecipient(teamMemberInfo);
			if (recipient == null)
			{
				return false;
			}

			var recipientFreeBusyCacheInfo = GetRecipientFreeBusyCacheInfo(recipient, dateTime);
			int index = (int) (dateTime.TimeOfDay.TotalMinutes / SlotSize.TotalMinutes);
			var state = ConvertFreeBusyTime(recipientFreeBusyCacheInfo.FreeBusyString[index]);

			return !this.freeBusyInfoConfiguration.IsConsideredFreeTime(state);
		}

		public bool HasFreeBusyInformationFor(ITeamMemberInformation teamMemberInfo)
		{
			return (GetRecipient(teamMemberInfo) != null);
		}

		public bool IsWorkDayFor(ITeamMemberInformation teamMemberInfo, DateTime dateTime)
		{
			if (this.freeBusyInfoConfiguration.ExcludeWeekends)
			{
				var dayOfWeek = dateTime.DayOfWeek;

				return (dayOfWeek != DayOfWeek.Saturday && dayOfWeek != DayOfWeek.Sunday);
			}

			return true;
		}

		public TimeSpan FreeBusySlotSize
		{
			[DebuggerStepThrough]
			get { return SlotSize; }
		}

		public void SetUp()
		{
			if (this.outlookSession == null)
			{
				try
				{
					this.outlookSession = new OutlookSession();
				}
				catch (OutlookIsolationException e)
				{
					throw new ConnectionException(Resources.Prompt_Exception_CouldNotConnectToOutlook, e);
				}
			}
		}

		private static FreeBusyTime ConvertFreeBusyTime(char c)
		{
			switch (c)
			{
				case '0':
					return FreeBusyTime.Free;
				case '1':
					return FreeBusyTime.Tentative;
				case '2':
					return FreeBusyTime.Busy;
				case '3':
					return FreeBusyTime.OutOfOffice;
				default:
					throw new ArgumentOutOfRangeException("c");
			}
		}

		private RecipientFreeBusyCacheInfo GetRecipientFreeBusyCacheInfo(IRecipient recipient, DateTime dateTime)
		{
			var date = dateTime.Date;
			Dictionary<DateTime, RecipientFreeBusyCacheInfo> recipientEntry;
			RecipientFreeBusyCacheInfo recipientFreeBusyCacheInfo = null;
			if (this.recipientFreeBusyDictionary.TryGetValue(recipient.Address, out recipientEntry))
			{
				if (recipientEntry.TryGetValue(date, out recipientFreeBusyCacheInfo))
				{
					if (IsCacheEntryOutdated(recipientFreeBusyCacheInfo))
					{
						recipientEntry.Remove(date);
						recipientFreeBusyCacheInfo = null;
					}
				}
			}

			if (recipientFreeBusyCacheInfo == null)
			{
				try
				{
					var todaysDate = date;
					int daysWorthOfSlots = 24 * 60 / (int) SlotSize.TotalMinutes;
					string largeFreeBusyString = recipient.FreeBusy(date, (int) SlotSize.TotalMinutes, true);

					while (largeFreeBusyString.Length >= daysWorthOfSlots)
					{
						string daysFreeBusyString = largeFreeBusyString.Substring(0, daysWorthOfSlots);
						largeFreeBusyString = largeFreeBusyString.Substring(daysWorthOfSlots);

						if (todaysDate == date)
						{
							recipientFreeBusyCacheInfo = AddRecipientFreeBusyInfo(recipient, todaysDate, daysFreeBusyString, ref recipientEntry);
						}
						else
						{
							AddRecipientFreeBusyInfo(recipient, todaysDate, daysFreeBusyString, ref recipientEntry);
						}

						todaysDate = todaysDate.AddDays(1.0).Date;
					}

					CleanupRecipientCache(recipientEntry);
				}
				catch (OutlookIsolationException e)
				{
					this.logger.Warn(e, "Exception detected while trying to communicate with Outlook.");

					throw new ConnectionException(e);
				}
			}

			return recipientFreeBusyCacheInfo;
		}

		private RecipientFreeBusyCacheInfo AddRecipientFreeBusyInfo(IRecipient recipient, DateTime day, string daysFreeBusyString,
		                                                            ref Dictionary<DateTime, RecipientFreeBusyCacheInfo> recipientEntry)
		{
			var recipientFreeBusyCacheInfo = new RecipientFreeBusyCacheInfo
			                                 	{
			                                 		Date = day,
			                                 		FreeBusyString = daysFreeBusyString,
			                                 		LastUpdate = SystemTime.Now()
			                                 	};
			if (recipientEntry == null)
			{
				recipientEntry = new Dictionary<DateTime, RecipientFreeBusyCacheInfo>();
				this.recipientFreeBusyDictionary.Add(recipient.Address, recipientEntry);
			}

			recipientEntry[day] = recipientFreeBusyCacheInfo;

			return recipientFreeBusyCacheInfo;
		}

		private static bool IsCacheEntryOutdated(RecipientFreeBusyCacheInfo recipientFreeBusyCacheInfo)
		{
			return (SystemTime.Now() - recipientFreeBusyCacheInfo.LastUpdate).TotalMinutes > 10.0;
		}

		private static void CleanupRecipientCache(IDictionary<DateTime, RecipientFreeBusyCacheInfo> recipientEntry)
		{
			List<DateTime> listOutdatedCacheEntries = null;
			foreach (var kvp in recipientEntry.Where(x => IsCacheEntryOutdated(x.Value)))
			{
				if (listOutdatedCacheEntries == null)
				{
					listOutdatedCacheEntries = new List<DateTime>();
				}
				listOutdatedCacheEntries.Add(kvp.Key);
			}
			if (listOutdatedCacheEntries != null)
			{
				listOutdatedCacheEntries.ForEach(x => recipientEntry.Remove(x));
			}
		}

		private IRecipient GetRecipient(ITeamMemberInformation teamMember)
		{
			IRecipient recipient = null;
			if (teamMember != null)
			{
				if (!this.recipientDictionary.TryGetValue(teamMember.Name, out recipient))
				{
					try
					{
						string outlookRecipientName = teamMember.SchedulerAlias;

						if (string.IsNullOrEmpty(outlookRecipientName))
						{
							outlookRecipientName = teamMember.Name;
						}

						recipient = this.outlookSession.CreateRecipient(outlookRecipientName);
						recipient.Resolve();
					}
					catch (OutlookIsolationException e)
					{
						this.logger.Warn(e, "Exception detected while trying to create recipient with Outlook.");

						throw new ConnectionException(e);
					}
				}

				try
				{
					if (recipient != null && !recipient.Resolved)
					{
						recipient = null;
					}
				}
				catch (OutlookIsolationException e)
				{
					this.logger.Warn(e, "Exception detected while trying to query recipient with Outlook.");

					recipient = null;
				}

				this.recipientDictionary[teamMember.Name] = recipient;
			}

			return recipient;
		}

		#endregion

		#region Nested type: RecipientFreeBusyCacheInfo

		private class RecipientFreeBusyCacheInfo
		{
			public DateTime Date { get; set; }
			public string FreeBusyString { get; set; }
			public DateTime LastUpdate { get; set; }
		}

		#endregion
	}
}